# Copyright 2010 Giacomo Alzetta
# This file is part of "LimberEditor" and is distribuhited under the
# terms of the MIT license.

'''
limber.documentmanager
======================

This module contains the classes and constants related
to the document managing system of LimberEditor.

Three constants are defined:

- SAVE
- CANCEL
- DISCARD

They are used to tell the `DocumentManager`_ class what to do
with files.

An exception class is supplied: *DocumentManagerError*.
Errors related to the managing of documents raise this
exception kind.


DocumentManager
---------------

This class implement the system of document managing.

Instances of this class are created from a *parent*(QObject)
and a *view_manager*::

    >>> from PyQt4 import QtCore
    >>> import viewmanager
    >>> parent = QtCore.QObject(None)
    >>> displayer = QtCore.QObject(None)
    >>> view_manager = viewmanager.ViewManager(parent, displayer)
    >>> doc_manager = DocumentManager(parent, view_manager)

The DocumentManager creates the documents and views.
Each document can have more than a view, while views
can have only a document associated with it.
'''

from PyQt4 import QtCore

SAVE = 0x1
CANCEL = 0x2
DISCARD = 0x3


class DocumentManagerError(Exception):
    '''Exception raised when occur some error in the
    managing of documents.
    '''

    pass


class DocumentManager(QtCore.QObject):
    '''This class is responsible for managing documents
    and their relationship with views.
    '''

    document_created = QtCore.pyqtSignal(name='documentCreated')
    view_created = QtCore.pyqtSignal(name='viewCreated')
    document_added = QtCore.pyqtSignal(name='documentAdded')
    document_saved = QtCore.pyqtSignal((unicode,),(unicode,unicode),
                                        name='documentSaved')
    view_closed = QtCore.pyqtSignal(name='viewClosed')
    document_closed = QtCore.pyqtSignal((unicode,),(unicode,unicode),
                                        name='documentClosed')

    def __init__(self, parent, view_manager):
        super(DocumentManager, self).__init__(parent)

        self._view_manager = view_manager

        self._docs_to_views = {}
        self._views_to_docs = {}

    def number_of_documents(self):
        '''Return the number of opened documents.'''

        return len(self._docs_to_views)

    def number_of_views(self):
        '''Return the number of opened views.'''

        return len(self._views_to_docs)

    def views(self, document):
        '''Return a tuple of views showing the given document.'''

        return tuple(self._docs_to_views[document])

    def _create_view(self,  document,  view_class):
        '''Creates and returns a view of the given class for the
        given document instance.
        '''

        view = view_class(self._view_manager.view_parent(), document)
        self._view_manager.add_view(view)
        view_title = document.title()
        doc_views = self._docs_to_views.get(document, [])
        if doc_views:
            view_title += ' %d' % (len(doc_views)+1)
        view.set_title(view_title)
        view.show()
        self.view_created.emit()
        return view

    def create_document(self, document_class, view_class):
        '''Creates a new document of the given class and shows it
        on a new view of the given class.
        '''

        document = document_class()
        view = self._create_view(document, view_class)
        self._docs_to_views.setdefault(document, []).append(view)
        self._views_to_docs[view] = document
        self.document_created.emit()
        return document

    def add_view(self, document, view_class):
        '''Adds a view of the given class to a managed document.'''

        if not document in self._docs_to_views:
            raise DocumentManagerError('The document is not managed by this manager.')

        view = self._create_view(document, view_class)
        self._docs_to_views[document].append(view)
        self._views_to_docs[view] = document
        return view

    def add_document(self, document, view_class):
        '''Adds an existing document to the document manager.'''

        view = self._create_view(document, view_class)
        self._docs_to_views.setdefault(document, []).append(view)
        self._views_to_docs[view] = document
        self.document_added.emit()
        return view

    def active_document(self):
        '''Returns the active document of the editor.'''

        return self._views_to_docs.get(self._view_manager.active_view(), None)

    def _save_document(self, document):
        '''Saves the current document.'''

        if document.path() is None:
            fname = self.parent().query_save_filename(document)
            if fname is False:
                return False
            document.set_path(fname)
        try:
            document.save()
            self._emit_document_saved(document)
            return True
        except Exception, e:
            self.parent().critical_error('Could not save the document %r:'
                                         % document.path(), exc=e)
            return False

    def _query_close_document(self, document):
        '''Queries the user what to do before closing the document.'''

        response = self.parent().query_close_document(document)
        if response == CANCEL:
            return False
        elif response == SAVE and document.is_modified():
            try:
                self._save_document(document)
                return True
            except Exception, e:
                should_discard = self.parent().query_discard_document(document,
                                  message='Could not save the document: %s\n%s'
                                          % (document.title(), e))
                return should_discard
        else:
            return True

    def _remove_view(self, document, view):
        '''Removes the given view.

            Raises DocumentManagerError if the view is not associated with the
            given document.
        '''

        try:
            self._docs_to_views[document].remove(view)
            del self._views_to_docs[view]
            self._view_manager.remove_view(view)
        except ValueError:
            raise DocumentManagerError('The view is not associated with the right document.')

    def close_view(self, view):
        '''Closes the given view. Returns True on success, False on failure.'''

        try:
            document = self._views_to_docs[view]
        except KeyError:
            raise DocumentManagerError('This view is not managed by this manager.')

        if len(self._docs_to_views[document]) > 1:
            self._remove_view(document, view)
            self.view_closed.emit()
            return True

        if self._query_close_document(document):
            self._remove_view(document, view)
            del self._docs_to_views[document]
            self.view_closed.emit()
            self._emit_document_closed(document)
            return True
        else:
            return False

    def close_document(self, document):
        '''Closes the given document. Returns True on success, False on failure.'''

        try:
            views = self._docs_to_views[document][:]
        except KeyError:
            raise DocumentManagerError('The given document is not '
                                       'managed by this manager.')

        for view in views:
            if not self.close_view(view):
                break
        else:
            self.document_closed.emit(document.path())
            return True
        return False

    def close_all_documents(self):
        '''Closes all the documents.'''

        for document in self._docs_to_views.keys():
            if not self.close_document(document):
                break
        else:
            return True
        return False

    def _emit_document_saved(self, document):
        '''Emits the documentSaved signal in all its overloadings.'''

        title = document.title()
        if not title:
            title = ''
        path = document.path()
        if not path:
            path = ''
        self.document_saved[unicode].emit(title)
        self.document_saved[(unicode, unicode)].emit(title, path)

    def _emit_document_closed(self, document):
        '''Emits the documentClosed signal in all its overloadings.'''

        title = document.title()
        if not title:
            title = ''
        path = document.path()
        if not path:
            path = ''
        self.document_closed[unicode].emit(title)
        self.document_closed[unicode,unicode].emit(title, path)
    
    def modified_documents(self):
        '''Return a sequence of all documents that have unsaved changes.'''
        
        return [document for document in self._docs_to_views
                                      if document.is_modified()]